原文链接

译者序:
Js 主要是通过 Event Looper 在 Js 在单线程的环境下实现其异步机制,怀着求真的动机阅读了不少 AlloyTeam 关于 setTimeout 以及阮一峰老师异步机制相关文章,结果还是疑惑重重,最终来到了 比较官方的 MDN。

JavaScript有一个基于 “event loop” 的并发模型。它完全不同于其他语言的模型,如 C 和 Java 。

运行时概念

接下来这部分主要解释一个理论模型。如今的 JavaScript 引擎已经在很大程度在描述语义化上实现并优化。

可视化表示

帧栈中调用函数

function foo(b) {
var a = 10;
return a + b + 11;
}

function bar(x) {
var y = 3;
return foo(x * y);
}

console.log(bar(7));

在调用 bar 函数时,第一帧被创建并包含了 bar 函数的参数和一个局部变量。当 bar 调用 foo 时,第二个帧被创建并且被压到包含了 bar 函数的参数和局部变量的帧顶。当 foo 调用被返回时,顶部的帧元素会被弹出栈 (仅离开 bar 函数的帧)。 当 bar 结束调用,栈则会变空。

对象将会被分配到堆内存(堆内存仅仅表示内存中大片的不规则的部分)

队列

一个 JavaScript 运行时包含一个将会被执行的一系列消息的消息队列。一个函数与每一个消息都有关。当栈内存拥有足够空间时,一个消息将会出队并被执行。执行流主要由调用相关函数 (这样就创建了一个初始状态的栈帧) 组成。当栈变空后消息的执行也就终止了。

事件循环机制

事件循环的命名源自它的实现,类似于如下代码:

while (queue.waitForMessage()) {
queue.processNextMessage();
}

queue.waitForMessage 方法会在当前没有消息时以同步的方式来等待消息的到来。

“运行直到完成”

每一个消息在其他消息被执行之前都会执行到结束。这为你在推导程序上提供了一些不错的特性,如一个函数的执行时机,它无法在其他代码运行前被提前调用并被完整执行(但可以修改该函数所控制的数据)。这不同于 C、譬如,当一个函数在一个线程中运行时,它可以在任何点被中止并且去其他线程执行其他代码。

这种模型的缺点就是如果一个消息占用太长时间才能完成时,web 应用就无法执行用户的类似于点击、滚动等交互。浏览器可以通过显示有 “脚本所占用太长时间而不能被运行” 的对话框来缓解。一个不错的方案就是在能够将一个大消息切为多个小消息的情况下尽量减小单位消息的大小。

添加消息

在 web 浏览器中,只要事件发生并且有事件监听器与之绑定时消息都会被添加。如果没有监听器,这个事件将会被丢失。因此点击一个带有事件句柄的节点时,将会添加一个消息到消息队列中,任意其他事件也是一样的。

调用 setTimeout 将会在倒计时时间达标后添加一个消息到队列中。当队列中没有其他消息时,这个消息则会被立即执行;然而,如果有很多消息时,setTimeout 消息则会等待其他消息执行完毕。于是乎 setTiemout 的第二个参数仅表示一个最小时间且不能被保证。

0 延时

0 延时并不是真正意味着回调会在 0 毫秒后立即执行。使用 0 延迟参数来调用 setTimtout 并不会在给定的时间间隔后立即执行回调函数。它的执行取决于队列中等待的任务的数量。在以下例子中 “this is just a message” 将会在回调执行之前被打印到控制台中,因为 “延时” 参数表示的运行时去执行请求的最小时间,并不是一个被保证的事件。

(function() {

console.log('this is the start');

setTimeout(function cb() {
console.log('this is a msg from call back');
});

console.log('this is just a message');

setTimeout(function cb1() {
console.log('this is a msg from call back1');
}, 0);

console.log('this is the end');

})();

// "this is the start"
// "this is just a message"
// "this is the end"
// "this is a msg from call back"
// "this is a msg from call back1"

多个运行时之间的通信

一个工作线程或一个跨域的 iframe 会拥有自己的栈、堆和消息队列。两个不同的运行时只能通过 postMessage 方法发送消息来通信。这个方法可以在其他运行时监听消息事件时给它们发送消息。

永不中断

事件循环模型的一个特别有趣的特性就是:JavaScript 不像其他很多语言那样,永远不会被中断。通过事件和回调处理 I/O 就比较典型,因此当一个应用在等待 IndexedDB 查询并返回 XHR 请求来结束运行时,它仍然可以执行类似于用户输入这样的其他事件。

像警告或同步 XHR 请求这样遗留下来的异常依然存在,但它依旧是一个避免它们的好的实践。注意,异常永远都会存在(但一般 都是实现上的 bug 而非其他)

待校对

donation